//  File:    page.tsx
//  Path:    apps/web/app/occupations/[code]/page.tsx
//  Project: AI Risk Index — airiskindex.io
//  Author:  Simon-Pierre Boucher
//  Contact: contact@spboucher.ai
//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
//
//  Description: Occupation detail: sub-score tiles, dimension breakdown, per-task rater audit trail.

import Link from "next/link";
import { notFound } from "next/navigation";
import { prisma } from "@airiskindex/db";
import {
  DIMENSIONS,
  HIGH_EXPOSURE_THRESHOLD,
  INVERTED_DIMENSIONS,
  WEIGHTS,
  pressure,
  type DimensionKey,
} from "@airiskindex/scoring";
import {
  DistributionChart,
  ScoreBar,
  ShareMeter,
  SubScoreDotPlot,
} from "@/components/score-marks";
import { formatWage, socGroupName } from "@/lib/soc-groups";

export const dynamic = "force-dynamic";

const DIMENSION_LABELS: Record<DimensionKey | "augmentation", string> = {
  automatability: "Task automatability",
  feasibility: "Technical feasibility today",
  cost_ratio: "Cost vs. human wage",
  barriers: "Adoption barriers",
  adoption_velocity: "Sector adoption velocity",
  augmentation: "Augmentation potential",
};

interface DimensionAggregate {
  dimension: DimensionKey;
  weight: number;
  inverted: boolean;
  pressure: number; // 0–100, orientation applied
  meanRating: number; // raw 1–5 panel mean
}

async function loadOccupation(code: string) {
  const occupation = await prisma.occupation.findUnique({
    where: { code },
    include: { tasks: { orderBy: { importance: "desc" } } },
  });
  if (!occupation) return null;

  const score = await prisma.occupationScore.findFirst({
    where: { occupationCode: code },
    orderBy: { run: { createdAt: "desc" } },
    include: { run: true },
  });

  const taskIds = occupation.tasks.map((task) => task.id);
  const [taskScores, ratings] = await Promise.all([
    score
      ? prisma.taskScore.findMany({ where: { runId: score.runId, taskId: { in: taskIds } } })
      : Promise.resolve([]),
    prisma.taskRating.findMany({
      where: { taskId: { in: taskIds } },
      select: { taskId: true, dimension: true, model: true, rating: true, rationale: true },
      orderBy: [{ dimension: "asc" }, { model: "asc" }],
    }),
  ]);

  let rank: number | null = null;
  let scoredTotal: number | null = null;
  if (score) {
    [rank, scoredTotal] = await Promise.all([
      prisma.occupationScore.count({
        where: { runId: score.runId, substitution: { gt: score.substitution } },
      }),
      prisma.occupationScore.count({ where: { runId: score.runId } }),
    ]);
    rank += 1;
  }

  const [related, indexScores] = await Promise.all([
    score
      ? prisma.occupationScore.findMany({
          where: {
            runId: score.runId,
            occupationCode: { startsWith: code.slice(0, 2), not: code },
          },
          orderBy: { substitution: "desc" },
          take: 5,
          include: { occupation: { select: { code: true, title: true } } },
        })
      : Promise.resolve([]),
    score
      ? prisma.occupationScore.findMany({
          where: { runId: score.runId },
          select: { substitution: true },
        })
      : Promise.resolve([]),
  ]);

  return { occupation, score, taskScores, ratings, rank, scoredTotal, related, indexScores };
}

function toBins(values: number[], binCount = 20): number[] {
  const bins = Array.from({ length: binCount }, () => 0);
  for (const value of values) {
    bins[Math.min(binCount - 1, Math.floor((value / 100) * binCount))] += 1;
  }
  return bins;
}

function aggregateDimensions(
  tasks: Array<{ id: string; importance: number | null }>,
  ratings: Array<{ taskId: string; dimension: string; rating: number }>,
): DimensionAggregate[] {
  const out: DimensionAggregate[] = [];
  for (const dimension of DIMENSIONS) {
    let weightSum = 0;
    let ratingSum = 0;
    for (const task of tasks) {
      const values = ratings
        .filter((row) => row.taskId === task.id && row.dimension === dimension)
        .map((row) => row.rating);
      if (values.length === 0) continue;
      const mean = values.reduce((a, b) => a + b, 0) / values.length;
      const weight = task.importance ?? 3;
      ratingSum += mean * weight;
      weightSum += weight;
    }
    if (weightSum === 0) continue;
    const meanRating = ratingSum / weightSum;
    out.push({
      dimension,
      weight: WEIGHTS[dimension],
      inverted: INVERTED_DIMENSIONS.has(dimension),
      pressure: 100 * pressure(dimension, meanRating),
      meanRating,
    });
  }
  return out;
}

export default async function OccupationPage({
  params,
}: {
  params: { code: string };
}): Promise<JSX.Element> {
  const data = await loadOccupation(params.code);
  if (!data) notFound();
  const { occupation, score, taskScores, ratings, rank, scoredTotal, related, indexScores } = data;

  const byTask = new Map(taskScores.map((entry) => [entry.taskId, entry]));
  const ratingsByTask = new Map<string, typeof ratings>();
  for (const row of ratings) {
    const list = ratingsByTask.get(row.taskId) ?? [];
    list.push(row);
    ratingsByTask.set(row.taskId, list);
  }
  const rankedTasks = [...occupation.tasks].sort(
    (a, b) => (byTask.get(b.id)?.substitution ?? -1) - (byTask.get(a.id)?.substitution ?? -1),
  );
  const dimensions = aggregateDimensions(occupation.tasks, ratings);
  const wage = formatWage(occupation.medianWageCents);

  return (
    <main className="mx-auto max-w-4xl px-4 py-10 sm:px-6 sm:py-12">
      <nav className="text-sm text-[var(--ink-2)]">
        <Link href="/#ranking" className="hover:text-[var(--ink)]">
          Ranking
        </Link>{" "}
        /{" "}
        <Link
          href={`/occupations#g${occupation.code.slice(0, 2)}`}
          className="hover:text-[var(--ink)]"
        >
          {socGroupName(occupation.code)}
        </Link>
      </nav>

      <div className="mt-4 flex flex-wrap items-baseline gap-x-3 gap-y-1">
        <h1 className="text-3xl font-bold tracking-tight">{occupation.title}</h1>
        <span className="rounded-full border border-[var(--border)] px-2.5 py-0.5 text-xs text-[var(--muted)]">
          {occupation.code}
        </span>
      </div>
      <div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-sm text-[var(--ink-2)]">
        {wage && <span>Median wage {wage}</span>}
        {occupation.employment != null && (
          <span>{occupation.employment.toLocaleString("en-US")} employed (US)</span>
        )}
        {rank != null && scoredTotal != null && (
          <span>
            Rank <strong className="text-[var(--ink)]">#{rank}</strong> of {scoredTotal} scored ·
            top {Math.max(1, Math.round((rank / scoredTotal) * 100))}% by substitution
          </span>
        )}
      </div>
      {occupation.description && (
        <p className="mt-3 max-w-2xl leading-relaxed text-[var(--ink-2)]">
          {occupation.description}
        </p>
      )}

      {!score ? (
        <div className="mt-10 card p-8 text-sm text-[var(--ink-2)]">
          This occupation has not been scored yet — its {occupation.tasks.length} tasks are queued
          for the multi-model rater panel. Task statements are listed below.
          <ul className="mt-4 list-inside list-disc space-y-1">
            {occupation.tasks.slice(0, 20).map((task) => (
              <li key={task.id}>{task.statement}</li>
            ))}
          </ul>
        </div>
      ) : (
        <>
          <section className="mt-10">
            <div className="card p-5 sm:p-6">
              <div className="flex flex-wrap items-baseline justify-between gap-2">
                <h2 className="text-lg font-semibold tracking-tight">Sub-scores</h2>
                <p className="text-xs text-[var(--muted)]">
                  0–100 · band = confidence interval from rater disagreement
                </p>
              </div>
              <div className="mt-4">
                <SubScoreDotPlot
                  rows={[
                    {
                      label: "Substitution",
                      band: {
                        low: score.substitutionLow,
                        score: score.substitution,
                        high: score.substitutionHigh,
                      },
                    },
                    {
                      label: "Exposure",
                      band: {
                        low: score.exposureLow,
                        score: score.exposure,
                        high: score.exposureHigh,
                      },
                    },
                    {
                      label: "Augmentation",
                      band: {
                        low: score.augmentationLow,
                        score: score.augmentation,
                        high: score.augmentationHigh,
                      },
                    },
                  ]}
                />
              </div>
              <div className="mt-5 grid gap-4 border-t border-[var(--grid)] pt-4 text-xs leading-relaxed text-[var(--muted)] sm:grid-cols-3">
                <p>
                  <strong className="font-medium text-[var(--ink-2)]">Substitution</strong> — the
                  headline: capability discounted by cost, barriers and adoption.
                </p>
                <p>
                  <strong className="font-medium text-[var(--ink-2)]">Exposure</strong> —
                  technical capability alone, regardless of whether anyone deploys it.
                </p>
                <p>
                  <strong className="font-medium text-[var(--ink-2)]">Augmentation</strong> — how
                  much AI assists without replacing. High here + moderate substitution = a
                  changing job, not a disappearing one.
                </p>
              </div>
            </div>
            <div className="mt-4 grid gap-4 lg:grid-cols-2">
              <div className="card min-w-0 p-5">
                <h3 className="text-sm font-semibold">Tasks on the substitution scale</h3>
                <p className="mt-0.5 text-xs text-[var(--muted)]">
                  {taskScores.length} rated tasks, binned by substitution score.
                </p>
                <div className="mt-4">
                  <DistributionChart bins={toBins(taskScores.map((t) => t.substitution))} />
                </div>
              </div>
              <div className="card min-w-0 p-5">
                <h3 className="text-sm font-semibold">Position among all scored occupations</h3>
                <p className="mt-0.5 text-xs text-[var(--muted)]">
                  Distribution of {indexScores.length.toLocaleString("en-US")} occupation scores;
                  the marker is this occupation.
                </p>
                <div className="mt-4">
                  <DistributionChart
                    bins={toBins(indexScores.map((s) => s.substitution))}
                    marker={score.substitution}
                    markerLabel={rank != null ? `#${rank} · ${score.substitution.toFixed(0)}` : score.substitution.toFixed(0)}
                  />
                </div>
              </div>
            </div>
            <div className="mt-4">
              <ShareMeter
                share={score.highlyExposedTaskShare}
                label={`Tasks with substitution ≥ ${HIGH_EXPOSURE_THRESHOLD}`}
              />
            </div>
            <p className="mt-3 text-xs text-[var(--muted)]">
              Run {score.run.indexVersion} · computed {score.run.createdAt.toISOString().slice(0, 10)} ·
              rater panel: {score.run.raterModels.join(", ")} · intervals span rater disagreement.
            </p>
          </section>

          {dimensions.length > 0 && (
            <section className="mt-12">
              <h2 className="text-xl font-semibold tracking-tight">Why this score</h2>
              <p className="mt-1 max-w-2xl text-sm text-[var(--ink-2)]">
                The five weighted dimensions of the composite, averaged across this occupation's
                tasks (importance-weighted, panel mean). Exact weights and formulas:{" "}
                <Link href="/api/v1/methodology" className="underline">
                  /api/v1/methodology
                </Link>
                .
              </p>
              <div className="mt-5 overflow-hidden card">
                {dimensions.map((dim) => (
                  <div key={dim.dimension} className="border-b border-[var(--grid)] px-5 py-3.5 last:border-b-0">
                    <div className="flex flex-wrap items-baseline gap-2">
                      <span className="text-sm font-medium">
                        {DIMENSION_LABELS[dim.dimension]}
                      </span>
                      <span className="rounded-full bg-[var(--seq-track)] px-2 py-0.5 text-[10px] font-semibold tabular-nums">
                        w {(dim.weight * 100).toFixed(0)}%
                      </span>
                      {dim.inverted && (
                        <span className="text-xs text-[var(--muted)]">
                          inverted — strong barriers lower the score
                        </span>
                      )}
                      <span className="ml-auto text-sm font-semibold tabular-nums">
                        {dim.pressure.toFixed(0)}
                      </span>
                    </div>
                    <div aria-hidden="true" className="mt-1.5 h-[6px] rounded-r-[3px] bg-[var(--seq-track)]">
                      <div
                        className="h-full rounded-r-[3px] bg-[var(--seq)]"
                        style={{ width: `${Math.min(100, dim.pressure)}%` }}
                      />
                    </div>
                    <p className="mt-1 text-xs text-[var(--muted)]">
                      panel mean rating {dim.meanRating.toFixed(1)}/5
                      {dim.inverted
                        ? " (barrier strength) → substitution pressure "
                        : " → substitution pressure "}
                      {dim.pressure.toFixed(0)}/100
                    </p>
                  </div>
                ))}
              </div>
            </section>
          )}

          <section className="mt-12">
            <h2 className="text-xl font-semibold tracking-tight">
              Task breakdown{" "}
              <span className="text-sm font-normal text-[var(--muted)]">
                ({rankedTasks.length} tasks)
              </span>
            </h2>
            <p className="mt-1 text-sm text-[var(--ink-2)]">
              Substitution pressure per task, weighted by O*NET importance in the composite.
              Expand a task for the full rater audit trail — every rating, every model, every
              rationale.
            </p>
            <div className="mt-5 overflow-hidden card">
              {rankedTasks.map((task) => {
                const ts = byTask.get(task.id);
                const taskRatings = ratingsByTask.get(task.id) ?? [];
                return (
                  <details key={task.id} className="group border-b border-[var(--grid)] last:border-b-0">
                    <summary className="cursor-pointer list-none px-5 py-4 hover:bg-[var(--wash)]">
                      <div className="flex items-baseline gap-3">
                        <p className="text-sm">{task.statement}</p>
                        <span className="ml-auto shrink-0 pl-3 text-sm font-semibold tabular-nums">
                          {ts ? ts.substitution.toFixed(0) : "—"}
                        </span>
                      </div>
                      <div className="mt-2">
                        {ts ? (
                          <ScoreBar
                            band={{
                              low: ts.substitutionLow,
                              score: ts.substitution,
                              high: ts.substitutionHigh,
                            }}
                            thick={8}
                          />
                        ) : (
                          <div className="h-[8px] rounded-[4px] bg-[var(--seq-track)]" />
                        )}
                      </div>
                      {ts && (
                        <p className="mt-1.5 text-xs text-[var(--muted)]">
                          CI {ts.substitutionLow.toFixed(0)}–{ts.substitutionHigh.toFixed(0)} ·
                          exposure {ts.exposure.toFixed(0)} · augmentation{" "}
                          {ts.augmentation.toFixed(0)}
                          {task.importance != null && <> · importance {task.importance.toFixed(1)}/5</>}
                          {taskRatings.length > 0 && <> · click for rater detail</>}
                        </p>
                      )}
                    </summary>
                    {taskRatings.length > 0 && (
                      <div className="overflow-x-auto border-t border-[var(--grid)] bg-[var(--page)] px-5 py-4">
                        <table className="w-full min-w-[480px] text-left text-xs">
                          <caption className="sr-only">
                            Panel ratings per dimension for this task
                          </caption>
                          <thead className="text-[var(--muted)]">
                            <tr>
                              <th className="py-1 pr-3 font-medium">Dimension</th>
                              <th className="py-1 pr-3 font-medium">Model</th>
                              <th className="py-1 pr-3 font-medium">Rating</th>
                              <th className="py-1 font-medium">Rationale</th>
                            </tr>
                          </thead>
                          <tbody className="align-top">
                            {taskRatings.map((row, index) => (
                              <tr
                                key={`${row.dimension}-${row.model}-${index}`}
                                className="border-t border-[var(--grid)]"
                              >
                                <td className="py-1.5 pr-3 whitespace-nowrap">
                                  {DIMENSION_LABELS[row.dimension as DimensionKey] ?? row.dimension}
                                </td>
                                <td className="py-1.5 pr-3 whitespace-nowrap text-[var(--muted)]">
                                  {row.model}
                                </td>
                                <td className="py-1.5 pr-3 font-semibold tabular-nums">
                                  {row.rating}/5
                                </td>
                                <td className="py-1.5 text-[var(--ink-2)]">{row.rationale}</td>
                              </tr>
                            ))}
                          </tbody>
                        </table>
                      </div>
                    )}
                  </details>
                );
              })}
            </div>
          </section>

          {related.length > 0 && (
            <section className="mt-12">
              <h2 className="text-xl font-semibold tracking-tight">
                Related occupations — {socGroupName(occupation.code)}
              </h2>
              <ul className="mt-4 overflow-hidden card">
                {related.map((row) => (
                  <li key={row.occupationCode} className="border-b border-[var(--grid)] last:border-b-0">
                    <Link
                      href={`/occupations/${row.occupationCode}`}
                      className="flex items-baseline gap-3 px-5 py-3 hover:bg-[var(--wash)]"
                    >
                      <span className="min-w-0 truncate text-sm">{row.occupation.title}</span>
                      <span className="ml-auto text-sm font-semibold tabular-nums">
                        {row.substitution.toFixed(0)}
                      </span>
                    </Link>
                  </li>
                ))}
              </ul>
            </section>
          )}

          <section className="mt-12 grid gap-4 sm:grid-cols-2">
            <div className="card p-6">
              <h2 className="font-semibold">How to read this</h2>
              <p className="mt-2 text-sm leading-relaxed text-[var(--ink-2)]">
                A high substitution score does not mean this job disappears — it means a large
                share of its current tasks face replacement pressure, so the mix of tasks is
                likely to change. High augmentation alongside substitution typically means the
                occupation reorganizes around the protected tasks. Wide confidence intervals mean
                the rater panel disagreed: treat those scores as open questions, not verdicts.
              </p>
            </div>
            <div className="card p-6">
              <h2 className="font-semibold">What would change this score</h2>
              <p className="mt-2 text-sm leading-relaxed text-[var(--ink-2)]">
                New model capabilities (automatability, feasibility), falling inference costs
                (cost ratio), regulation and licensing shifts (barriers), and measured sector
                adoption (velocity) all re-enter at every index release. Each release is
                recomputed, versioned and kept queryable — scores are claims with a date on them,
                not permanent labels.
              </p>
            </div>
          </section>
        </>
      )}
    </main>
  );
}
